/**
 * 
 */
package controller;

import java.util.Arrays;

import components.DevLst;
import components.Util;
import components.IOBuffer.InputBuffer;
import components.exceptions.BinaryToIntException;
import components.exceptions.IntToBinaryException;
import components.exceptions.SubArrayException;
import components.exceptions.UnexpectedNotifyReceivedException;
import components.functionUnits.ALU;

import controller.notify.Notify;

/**
 * @author Chz
 * 
 */
public class ExecutionController extends Controller {

	private static ExecutionController self;
	private Notify notify = null;

	private ExecutionController() {

	}

	public static ExecutionController getInstance() {
		if (self == null)
			self = new ExecutionController();

		return self;
	}

	public synchronized void notify(Notify n) {
		this.notify = n;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see controller.ControllerListener#notify(controller.notify.Notify)
	 */
	@Override
	public void run() {
		switch (notify.getType()) {
		case Notify.DONE:
			// check OR and chose correct execution unit.
			try {
				execute(regs.getOpcode());
			} catch (BinaryToIntException e) {
				// TODO Auto-generated catch block
				System.err.println(e.getLocalizedMessage());
				e.printStackTrace();
			}
			break;
		case Notify.READ:
			System.err.println("Notify:READ received in Execution Controller!");
		case Notify.WRITE:
			System.err
					.println("Notify:WRITE received in Execution Controller!");
		default:
			System.err
					.println("fatal error in ExecutionController, undefined Notify type "
							+ "received!!!!!!!!!!!!!!!!");

		}
		bus.sendNotify(new Notify(Notify.DONE), PCController.getInstance());
	}

	/**
	 * @param opcode
	 * @throws BinaryToIntException
	 * 
	 */
	private synchronized void execute(int opcode) throws BinaryToIntException {

		// this three variables is declared for convince use.
		// please remember to initialize them before use.
		int intTmp1, intTmp2;
		int[] arrayTmp1, arrayTmp2;
		// DBG
		// System.out.println("in exec() with opcode: " + opcode);
		System.err.println("in exec() with opcode: " + opcode);
		switch (opcode) {

		/****************************/
		/** load/Store instructions */
		/****************************/
		case 1: // LDR
			// C(RAR1) = C(MBR)
			intTmp1 = Util.binaryToInt(regs.getRAR1());
			regs.setGPR(regs.getMBR(), intTmp1);

			notifyPCToInc();
			break;
		case 2: // STR
			// C(MBR) = C(RAR1)
			intTmp1 = Util.binaryToInt(regs.getRAR1());
			regs.setMBR(regs.getGPR(intTmp1));

			// MEMORY.WRITE
			notifyMemoryToWrite();
			break;
		case 3: // LDA
			// C(RAR1) = C(MAR)
			intTmp1 = Util.binaryToInt(regs.getRAR1());
			regs.setGPR(regs.getMAR(), intTmp1);

			notifyPCToInc();
			break;
		case 41: // LDX TODO: ask professor
			// C(X0) = C(MBR)
			regs.setX0(regs.getMBR());
			notifyPCToInc();
			break;
		case 42: // STX
			// C(MBR) = C(X0) TODO: ask professor
			regs.setMBR(regs.getX0());
			notifyMemoryToWrite();
			break;

		/**************************/
		/** Transfer Instructions */
		/**************************/

		case 10: // JZ

			// tmp1 is rar1, tmp2 = c(rar1)
			intTmp1 = Util.binaryToInt(regs.getRAR1());
			intTmp2 = Util.binaryToInt(regs.getGPR(intTmp1));

			if (intTmp2 == 0) {
				regs.setPC(regs.getMAR());
				notifyPCNoInc();
			} else
				notifyPCToInc();
			break;

		case 11: // JNZ
			// tmp1 is rar1, tmp2 = c(rar1)
			intTmp1 = Util.binaryToInt(regs.getRAR1());
			intTmp2 = Util.binaryToInt(regs.getGPR(intTmp1));

			if (intTmp2 != 0) {
				regs.setPC(regs.getMAR());
				notifyPCNoInc();
			} else
				notifyPCToInc();
			break;

		case 12: // JCC

			// tmp1 is CC
			intTmp1 = Util.binaryToInt(regs.getCC());

			if (intTmp1 == 1) {
				regs.setPC(regs.getMAR());
				notifyPCNoInc();
			} else
				notifyPCToInc();
			break;

		case 13: // JMP

			regs.setPC(regs.getMAR());
			notifyPCNoInc();
			break;

		case 14: // JSR
			// tmp1 is PC
			intTmp1 = Util.binaryToInt(regs.getPC());
			// R3 = PC++;
			try {
				regs.setGPR(Util.intToBinary(intTmp1 + 1, 16), 3);
				// here tmp1 changed, but we don't need to update PC, since
				// pc will be modified manually latter in this function.
			} catch (IntToBinaryException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
			// PC = C(MAR)
			regs.setPC(regs.getMAR());
			notifyPCNoInc();
			break;

		case 15: // RFS
			// R0 = immed : already done in decoder.
			// PC = C(R3)
			regs.setPC(regs.getGPR(3));
			notifyPCNoInc();
			break;

		case 16: // SOB

			// tmp1 is rar1, tmp2 = c(rar1)
			intTmp1 = Util.binaryToInt(regs.getRAR1());
			intTmp2 = Util.binaryToInt(regs.getGPR(intTmp1));

			// c(r) = c(r)-1
			intTmp2--;
			// write tmp2 back to c(r)
			try {
				regs.setGPR(Util.intToBinary(intTmp2, 16), intTmp1);
			} catch (IntToBinaryException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}

			// if c(r) > 0, then pc = ea
			if (intTmp2 > 0) {
				// PC = C(MAR)
				regs.setPC(regs.getMAR());
				notifyPCNoInc();
			} else
				notifyPCToInc();
			break;

		/**********************************/
		/** Arithmetic/Logic Instructions */
		/**********************************/

		case 4: // AMR
			// tmp1 is r, getGPR(tmp1) = c(r)
			intTmp1 = Util.binaryToInt(regs.getRAR1());

			// r = c(r) + c(EA)
			regs.setGPR(ALU.add(regs.getGPR(intTmp1), regs.getMBR()), intTmp1);
			notifyPCToInc();
			break;

		case 5: // SMR
			// tmp1 is r, getGPR(tmp1) = c(r)
			intTmp1 = Util.binaryToInt(regs.getRAR1());

			// r = c(r) + c(EA)
			regs.setGPR(ALU.sub(regs.getGPR(intTmp1), regs.getMBR()), intTmp1);
			notifyPCToInc();
			break;

		case 6: // IAR
			// tmp1 is r, getGPR(tmp1) = c(r)
			intTmp1 = Util.binaryToInt(regs.getRAR1());

			// r = c(r) + immed, immed is in R0
			regs.setGPR(ALU.add(regs.getGPR(intTmp1), regs.getIMMR()), intTmp1);
			// DBG
			System.err.println("r1 is:\t\t " + Arrays.toString(regs.getGPR(0)));
			notifyPCToInc();
			break;

		case 7: // ISR
			// tmp1 is r, getGPR(tmp1) = c(r)
			intTmp1 = Util.binaryToInt(regs.getRAR1());

			// r = c(r) + immed, immed is in R0
			regs.setGPR(ALU.sub(regs.getGPR(intTmp1), regs.getIMMR()), intTmp1);
			notifyPCToInc();
			break;

		/***************************/
		/** immediate Instructions */
		/***************************/

		case 20: // MUL
			// rx, rx+1 <= c(rar1) * c(rar2)
			arrayTmp1 = new int[32]; // hold rx and rx+1
			arrayTmp1 = ALU.mul(regs.getGPR(regs.getRAR1InInt()),
					regs.getGPR(regs.getRAR2InInt()));
			try {
				regs.setGPR(Util.subArray(arrayTmp1, 0, 15),
						regs.getRAR1InInt()); // store the higher order bits
												// into
												// rx
				regs.setGPR(Util.subArray(arrayTmp1, 16, 31),
						regs.getRAR1InInt() + 1); // store the lower order bits
													// into rx+1
			} catch (SubArrayException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
			break;

		case 21: // DIV
			// rx, rx+1 <- c(rx)/ c(ry)
			arrayTmp1 = new int[32]; // hold rx and rx+1
			arrayTmp1 = ALU.div(regs.getGPR(regs.getRAR1InInt()),
					regs.getGPR(regs.getRAR2InInt()));
			try {
				regs.setGPR(Util.subArray(arrayTmp1, 0, 15),
						regs.getRAR1InInt()); // store the quotient in higher
												// order bits into rx
				regs.setGPR(Util.subArray(arrayTmp1, 16, 31),
						regs.getRAR1InInt() + 1); // store the remainder in
													// lower order bits into
													// rx+1
			} catch (SubArrayException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
			break;

		case 22: // TST
			// If c(rx) = c(ry), set cc(4)=1; else, cc(4) = 0
			arrayTmp1 = new int[16];
			arrayTmp2 = new int[16];
			arrayTmp1 = regs.getGPR(regs.getRAR1InInt()); // arrayTmp1 =
															// c(rar1);
			arrayTmp2 = regs.getGPR(regs.getRAR2InInt()); // arrayTmp1 =
															// c(rar1);
			regs.setCC(4, ALU.tst(arrayTmp1, arrayTmp2));
			break;

		case 23: // AND
			// c(rx) <= c(rx) AND c(ry);
			arrayTmp1 = new int[16];
			arrayTmp2 = new int[16];
			arrayTmp1 = regs.getGPR(regs.getRAR1InInt()); // arrayTmp1 =
															// c(rar1);
			arrayTmp2 = regs.getGPR(regs.getRAR2InInt()); // arrayTmp1 =
															// c(rar1);
			regs.setGPR(ALU.and(arrayTmp1, arrayTmp2), regs.getRAR1InInt());
			break;
		case 24: // OR
			// c(rx) <= c(rx) OR c(ry);
			arrayTmp1 = new int[16];
			arrayTmp2 = new int[16];
			arrayTmp1 = regs.getGPR(regs.getRAR1InInt()); // arrayTmp1 =
															// c(rar1);
			arrayTmp2 = regs.getGPR(regs.getRAR2InInt()); // arrayTmp1 =
															// c(rar1);
			regs.setGPR(ALU.or(arrayTmp1, arrayTmp2), regs.getRAR1InInt());
			break;

		case 25: // NOT
			// c(rx) <= NOT c(rx);
			arrayTmp1 = new int[16];
			arrayTmp1 = regs.getGPR(regs.getRAR1InInt()); // arrayTmp1 =
															// c(rar1);
			regs.setGPR(ALU.not(arrayTmp1), regs.getRAR1InInt());
			break;

		/******************************/
		/** shift/rotate Instructions */
		/******************************/

		case 31: // SRC
			// TODO: to be tested
			arrayTmp1 = new int[16];
			arrayTmp2 = new int[16];
			arrayTmp1 = regs.getGPR(regs.getRAR1InInt()); // arrayTmp1 =
															// c(rar1);

			// shift result stored in arrayTmp2
			arrayTmp2 = ALU.src(arrayTmp1, regs.getLRR(), regs.getALR(),
					regs.getCNTRInInt());

			// put the shifted result into r(rar1)
			regs.setGPR(arrayTmp2, regs.getRAR1InInt());
			break;
		case 32: // RRC
			// TODO: to be tested
			arrayTmp1 = new int[16];
			arrayTmp2 = new int[16];
			arrayTmp1 = regs.getGPR(regs.getRAR1InInt()); // arrayTmp1 =
															// c(rar1);

			// shift result stored in arrayTmp2
			arrayTmp2 = ALU.rrc(arrayTmp1, regs.getLRR(), regs.getALR(),
					regs.getCNTRInInt());

			// put the shifted result into r(rar1)
			regs.setGPR(arrayTmp2, regs.getRAR1InInt());

			break;

		/****************************/
		/** device I/O Instructions */
		/****************************/

		case 61: // IN
			// input character to register from device

			// first get content from device buffer
			arrayTmp1 = new int[16];
			arrayTmp1 = DevLst.getInstance().list.get(0).flush();

			// then, set the content into target register. The target register
			// index is stored in RAR1
			regs.setGPR(arrayTmp1, regs.getRAR1InInt());

			break;
		case 62: // OUT
			// output character to device from register

			// first get content from register. the index of the register is
			// stored in RAR1
			arrayTmp1 = new int[16];
			arrayTmp1 = regs.getGPR(regs.getRAR1InInt());

			// then send the content of register to target buffer.
			intTmp1 = 0;
			for (; intTmp1 < 16; intTmp1++) {
				String ascByte = arrayTmp1[intTmp1] + "";
				DevLst.getInstance().list.get(1).putIntoBuffer(
						ascByte.toCharArray()[0]);
			}

			DevLst.getInstance().list.get(1).flush();

			// DBG
			// System.out
			// .println("console printer: " + Arrays.toString(arrayTmp1));
			System.err
					.println("console printer: " + Arrays.toString(arrayTmp1));
			break;
		case 63: // CHK
			// TODO: to be finished.
			break;

		default:
			// we shouldn't reach here!
			System.err.println("opcode: " + opcode + "isn't handle correctly,"
					+ " please check the source code in function execute() in "
					+ "controller/ExecutionController.java");
		}
	}

	private void notifyMemoryToWrite() {
		bus.sendNotify(new Notify(Notify.WRITE), MemoryController.getInstance());
	}

	private void notifyPCToInc() {
		bus.sendNotify(new Notify(Notify.DONE), PCController.getInstance());
	}

	private void notifyPCNoInc() {
		bus.sendNotify(new Notify(Notify.NOINC), PCController.getInstance());
	}

}
